#import "template.typ": project
#import "codly.typ": *
#import "@preview/fletcher:0.4.2" as fletcher: node, edge
#import fletcher.shapes: house, hexagon, diamond

#show: codly-init.with()

#codly(languages: (c: (name: "C", color: rgb("#CE412B"))))

#show raw: set text(font: ("FiraMono Nerd Font Mono", "Source Han Sans"))

#show link: set text(fill: blue)

#show: project.with(
  title: "实现一个自定义的 DNS 请求工具",
  authors: ((name: "徐一航", number: "2019212011"), (name: "李睿", number: "2021211541")),
  teacher: "张树壮",
  course: "网络协议分析实践",
)

= 实践内容

== 目标
实现一个自定义的 DNS 请求工具

1 程序名称：my_nslookup

2 支持如下参数：

必选参数： DNS 服务器 IP 地址，域名

可选参数： 本机 mac 地址，网关 mac 地址，本机 IP 地址，UDP 源端口

3 程序能够向指定的 DNS 服务器发送域名请求，并解析服务的应答。

#par(first-line-indent: 2em)[
  3.1 需根据协议的格式自行构造请求数据包的：应用层（DNS 协议），传输层（UDP），网络层（IP），链路层（以太网），对发送数据包各层的主要字段进行打印。
  
  3.2 使用数据包发送 API 将数据包发送到网络上，并捕获响应包
  
  3.3 对响应包各层的主要字段进行打印。
  
  3.4 将请求数据包和响应数据包保存成：pcap 文件，并使用 wireshark 进行验证。
]

== 原理
libpcap有这些功能：
#pad(left: 2em)[
  - 捕获原始数据包，包括发往正在运行的计算机的数据包以及由其他主机（在共享介质上）交换的数据包
  - 在将数据包分派到应用程序之前根据用户指定的规则过滤数据包
  - 将原始数据包传输到网络
  - 收集有关网络流量的统计信息
]

在windows平台上，Npcap库提供了这些API。使用这些API来实现我们的程序。

== 程序的主要功能
#pad(left: 2em)[
  - 读取和分析类似nslookup的命令行参数。
  - 利用命令行参数取得的信息或默认信息，组装数据包，发送查询包。
  - 分析收到的回复包，打印数据包信息。
  - 主循环。启动抓包，保存抓包文件，调用前面的三个部分。
]

== 使用方法

使用方法和 nslookup 基本一致，但是只有基本功能。

#codly(enable-numbers: false)

=== 交互模式

#pad(left: 2em)[
  无参数启动程序或仅指定 DNS 服务器地址时，进入交互模式。可以连续地进行查询。例： ```
  .\my_nslookup
  ``` ```
  .\my_nslookup 114.114.114.114
  ```
]

=== 非交互模式
#pad(left: 2em)[
  以待查询的域名为第一个参数时，进入非交互模式。打印查询结果后直接退出。例： ```
  .\my_nslookup baidu.com
  ```
]

=== 其他可选参数

#pad(left: 2em)[
  其他参数的格式为 `-<option>=<VALUE>`。可以输入要使用的端口等信息。例： ```
 #设置源端口为4321
 .\my_nslookup -p=4321 
 ``` ```
  #显示帮助信息
  .\my_nslookup -h 
  ``` 虽然可以用 `-i`、`-m` 参数自定义源 IP 地址、源 MAC 地址，但不建议这么做。可能导致之后收不到 DNS 服务器给的回应数据包。
]

=== 交互模式命令
#pad(left: 2em)[
  ```
  > exit  #退出程序
  ```
  ```
  > help  #显示帮助信息
  ```
]

#set par(first-line-indent: 2em)

= 数据包构造
== 以太网帧头部：
包含以下字段：

目的MAC地址：接收数据包的设备的物理地址。

源MAC地址：发送数据包的设备的物理地址。

协议类型：表示封装在以太网帧中的协议类型。对于IPv4，这个值是0x0800。
== IP头部：
包含以下字段：

版本：IP协议的版本，IPv4或IPv6。

头部长度：IP头部的长度。

总长度：整个IP数据包的长度。

生存时间（TTL）：限制数据包在网络中的生存时间。

协议：封装在IP数据包中的协议类型。对于UDP，这个值是17。

源IP地址：发送数据包的设备的IP地址。

目的IP地址：接收数据包的设备的IP地址。

== UDP头部：
包含以下字段：

源端口：发送数据包的设备的端口号。

目的端口：接收数据包的设备的端口号。对于DNS查询，这个值通常是53。

长度：UDP头部和数据的总长度。

校验和：用于错误检测的校验和。

== DNS消息：
包含DNS头部、问题部分、回答部分、授权部分和附加信息部分。
=== DNS头部：
包含以下字段：

Transaction ID：一个16位的字段，用于标识请求和响应。

Flags：包含多个子字段，如QR（查询/响应标志）、Opcode（操作码）、AA（授权回答标志）、TC（截断标志）、RD（期望递归标志）、RA（可用递归标志）、Z（保留字段）和Rcode（返回码）。

Questions：一个16位的字段，表示问题部分中的问题数量。

Answer RRs：一个16位的字段，表示回答部分中的资源记录数量。

Authority RRs：一个16位的字段，表示授权部分中的资源记录数量。

Additional RRs：一个16位的字段，表示附加信息部分中的资源记录数量。

=== 问题部分：
包含一个或多个问题。每个问题包含以下字段：

QName：查询的域名。

QType：查询类型，如A（IPv4地址）、AAAA（IPv6地址）、MX（邮件交换记录）等。

QClass：查询类，通常为IN（Internet）

=== 回答部分：
包含一个或多个资源记录（RR）。每个资源记录包含以下字段：

Name：域名。

Type：记录类型，如A、AAAA、MX等。

Class：记录类，通常为IN。

TTL：生存时间，表示资源记录的有效期。

RDLength：资源数据的长度。

RData：资源数据，其格式和内容取决于Type和Class。

// #set par(first-line-indent: 0em)

=== 授权部分和附加信息部分：
包含一个或多个资源记录，提供额外的信息，如权威DNS服务器的IP地址。

= 主要流程

程序分为四个部分：命令行参数解析、发送数据包、接收数据包、解析数据包。

== 主函数

读取接收命令行参数，将参数传递给解析函数，返回需要的配置信息（如待查询域名、源端口号等）。创建一个子线程，分别在两个线程上启动发包函数和抓包函数，再将配置信息传递给它们。

#let blob(pos, label, tint: white, ..args) = node(
  pos,
  align(center, label),
  fill: tint.lighten(60%),
  stroke: 1pt + tint.darken(20%),
  corner-radius: 5pt,
  ..args,
)

#figure(
  fletcher.diagram(
    spacing: 12pt,
    cell-size: (12mm, 10mm),
    edge-stroke: 1pt,
    edge-corner-radius: 4pt,
    mark-scale: 70%,
    blob((1, 0), [stdin], tint: green),
    edge((1, 0), (1, 2), label: "options", "-|>"),
    blob((1, 1.5), [config_parser()], tint: yellow, shape: hexagon),
    edge((1, 1.5), (1, 2.7), label: "config"),
    edge((1, 2.7), "l,dd", "-|>"),
    edge((1, 2.7), "r,dd", "-|>"),
    blob((0, 4), [packet_sender()], tint: yellow, shape: hexagon),
    blob((2, 4), [packet_reciver()], tint: yellow, shape: hexagon),
    edge((0, 4), "ddd,r", "--|>", label: "packet"),
    edge((1, 7), "r,uuu", "--|>", label: [#move(dx: 28pt, dy: 10pt, "packet")]),
    edge((0, 4), (2, 4)),
    edge((1, 4), (1, 4)),
    edge((1, 4), (1, 5.5), "-|>", label: "output"),
    blob((1, 5.5), [stdout], tint: green),
    blob((1, 7), [DNS server], tint: red, corner-radius: 0pt),
    edge((2, 4), (4, 4), "-|>"),
    blob((4, 4), [交互模式？], tint: blue, shape: diamond),
    edge((4, 4), (4, 2.7), (2.5, 2.7), (2.5, 3), label: "yes", "-|>"),
    blob((4, 5.5), [exit], tint: yellow, shape: hexagon),
    edge((4, 4), (4, 5.5), "-|>", label: "no"),
    edge((2.1, 4), (2.5, 3), bend: -54deg, "..", label: [#move(dx: -35pt, "loop")]),
    edge((2.5, 3), (2.1, 4), bend: -55deg, "..|>"),
    edge((1, 0), (-0.1, 0), "dddd", "..|>", label: "domain"),
  ),
  caption: [main() 函数],
)

主函数还要根据输入参数中是否包含待查询的域名来决定程序是否进入交互模式。如果进入非交互模式，则抓取到一个数据包后程序就退出。如果进入交互模式，则可以连续地对不同域名进行查询。

== 命令行参数解析

=== 处理选项参数（option arguments）

使用#link("https://likle.github.io/cargs/")[cargs]库来解析选项。cargs是一个轻量级的C/C++命令行参数解析器库，可用于解析传递给主函数的argv和argc参数。@图2
是本程序的帮助信息，其中Options: 部分内容由cargs自动生成。

=== 处理非选项参数（non-option arguments）

手动处理非选项参数。步骤如下：

==== 读第一个参数

如果没有参数，使用默认ip地址 -> 进入交互模式

否则，如果是ip地址 -> 进入交互模式

如果不是，则认为是域名，继续读取参数
#figure(image("carbon (6).png"), caption: [ `--help` 选项 ])<图2>
==== 读第二个参数

如果没有第二个参数，使用默认ip地址 -> 进入非交互模式

否则，如果是ip地址，继续读取参数

如果不是ip地址，则输出“不合法的服务器地址”，使用默认地址，继续读取参数

==== 读第三个参数

如果没有第三个参数 -> 进入非交互模式

如果有第三个参数，则输出“参数太多”，输出帮助信息 -> 结束程序

== 发送数据包

这个部分的关键部分是计算校验和的顺序。UDP校验和在计算UDP包长度之后计算，而IP校验和在计算IP包总长度之后计算。这是因为UDP和IP校验和都需要包含对应的长度字段。以下是发包函数的主要流程：

+ 首先，函数获取系统中的网络设备，并找到正在使用的设备。

+ 然后，函数创建一个以太网帧头部，并填充目标和源MAC地址。

+ 接下来，函数创建一个IP包头部，并填充源和目标IP地址。

+ 然后，函数创建一个UDP包头部，并填充源端口。

+ 接下来，函数创建一个DNS包头部，并填充查询ID。

+ 然后，函数创建一个DNS查询部分，包括查询的域名、查询类型和查询类别。

+ 接下来，函数计算并填充UDP包的长度。

+ 然后，函数创建一个伪头部，用于计算UDP校验和。伪头部包括源和目标IP地址、零、协议和UDP长度。函数使用伪头部、UDP包头部和DNS负载（包括DNS头部、查询、查询类型和查询类别）来计算UDP校验和，并填充到UDP包头部。

+ 接下来，函数计算并填充IP包的总长度。

+ 然后，函数计算并填充IP包头部的校验和。

+ 最后，函数创建一个包含以太网帧、IP头、UDP头和DNS负载的完整包，并发送这个包。

// #pagebreak()

== 接收和解析数据包

首先按照 @图3 流程抓取数据包，在`pcap_loop()`函数中传入回调函数`packet_handler()`来解析抓取到的包。

#let mynode(pos, label, tint: white, ..args) = node(
  pos,
  align(center, label),
  fill: tint.lighten(60%),
  width: 70mm,
  stroke: 1pt,
  ..args,
)

#figure(
  fletcher.diagram(
    spacing: 12pt,
    cell-size: (8mm, 8mm),
    edge-stroke: 1pt,
    edge-corner-radius: 4pt,
    mark-scale: 70%,
    mynode((0, 0), [获得所有网络设备列表\ pcap_findalldevs_ex()]),
    edge("-|>"),
    mynode((0, 1), [查找可用的网络设备\ find_default_dev_name()]),
    edge("-|>"),
    mynode((0, 2), [打开网络设备，获得捕获描述符\ pcap_open()]),
    edge("-|>"),
    mynode((0, 3), [编译过滤规则为内核程序\ pcap_compile()]),
    edge("-|>"),
    mynode((0, 4), [设置过滤程序\ ppcap_setfilter()]),
    edge("-|>"),
    mynode((0, 5), [设置文件保存路径\ pcap_dump_open()]),
    edge("-|>"),
    mynode((0, 6), [捕获包并处理\ pcap_loop()]),
    edge("-|>"),
    mynode((0, 7), [关闭并返回\ pcap_close()]),
  ),
  caption: [抓包基本流程],
)<图3>
处理数据包的函数`packet_handler()`在打印相关字段信息之前，需要先将字段的网络字节序转换为本机字节序。这部分使用了`winsock2.h`头文件。

= 实验结果

首先运行本程序，并发送几个查询。结果如 @图4 所示。使用 wireshark 工具，对运行本程序所产生的流量进行捕获，捕获结果如 @图5。
#figure(image("carbon (5).png", width: 93%), caption: [程序运行示例])<图4>

#figure(image("屏幕截图 2024-03-27 165243.png"), caption: [wireshark抓取到的包])<图5>

== 程序与服务器的协议交互过程

程序首先在本地构建一个DNS查询包，然后通过网络发送给DNS服务器。DNS服务器收到查询包后，会查找对应的记录，并将结果返回给程序。详细的交互过程如下：

- 数据链路层（Ethernet）：程序首先构建一个以太网帧，包含源和目标MAC地址。这个以太网帧会通过网络接口卡（NIC）发送到交换机或路由器。

- 网络层（IP）：在以太网帧的数据部分，程序构建一个IP包，包含源和目标IP地址。如果目标IP地址不在本地网络中，路由器会将IP包转发到下一个路由器，直到到达目标网络。在目标网络中，路由器会将IP包转发到目标IP地址对应的设备。

- 传输层（UDP）：在IP包的数据部分，程序构建一个UDP包，包含源和目标端口。在目标设备上，网络栈会根据目标端口将UDP包转发到对应的应用程序，这里是DNS服务器。

- 应用层（DNS）：在UDP包的数据部分，程序构建一个DNS查询。DNS服务器收到查询后，会查找对应的记录，并将结果封装在一个DNS回答中，然后发送回程序。

- 回答的传输：DNS服务器的回答会通过相同的层次结构发送回来：首先封装在一个UDP包中，然后封装在一个IP包中，最后封装在一个以太网帧中。设备上的网络栈会解封装这些包，并将DNS回答转发到程序。

- 解析DNS回答：程序接收并解析DNS回答，获取查询的结果。

以下是对系统的运行界面进行截图，以及对关键字段的值进行标注。
#figure(image("屏幕截图 2024-03-27 173155.png", width: 71%), caption: [字段值标注1])
#figure(image("屏幕截图 2024-03-27 172909.png", width: 78%), caption: [字段值标注2])

#set par(first-line-indent: 0em)
= 附录

- 项目仓库：

主仓库：#link("https://gitee.com/xixixiyu/my_nslookup")[戏鱼 / my_nslookup]

发送数据包部分：#link("https://gitee.com/xixixiyu/lookup")[戏鱼 / lookup]

- 参考文章：

#link("https://c.biancheng.net/view/6457.html")[DNS 报文格式解析]

#link("https://www.cnblogs.com/chiaki/p/13554706.html")[UDP 协议首部校验和]

#link("https://www.zhihu.com/question/47025566/answer/103966103")[计算校验和代码解析，包括字节序问题]

#link("https://en.cppreference.com/w/c/thread")[C11 并发库]

#link("https://blog.csdn.net/lwz15071387627/article/details/88132835")[互斥锁与条件变量详解]